package iceberg.domain;

import iceberg.rules.ArchitectureRule;
import iceberg.LayerCache;
import iceberg.rules.WrongLayerForFunctionalityRule;
import iceberg.constraints.*;
import iceberg.violations.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

public class StructureDefinition implements ConstraintResolver {

	private static final Logger LOG = LoggerFactory.getLogger(StructureDefinition.class);

	private final Map<String, ConstraintException> exceptions = new HashMap<String, ConstraintException>();
	private final LayerCache cache = new LayerCache();
	private final List<Rule> rules = new ArrayList<Rule>();
	private Set<Layer> layers;

	public StructureDefinition() {
		rules.add(new ArchitectureRule(this));
		rules.add(new WrongLayerForFunctionalityRule(this, "javax.swing", SwingViolationType.class, Functionality.GUI, true));
		rules.add(new WrongLayerForFunctionalityRule(this, "java.awt", AWTViolationType.class, Functionality.GUI, true));
		rules.add(new WrongLayerForFunctionalityRule(this, "java.io", IOViolationType.class, Functionality.DOMAIN, false));
		rules.add(new WrongLayerForFunctionalityRule(this, "java.io", IOViolationType.class, Functionality.GUI, false));
		rules.add(new WrongLayerForFunctionalityRule(this, "javax.persistence", PersistenceViolationType.class, Functionality.GUI, false));
		rules.add(new FormattingRule(this));
	}

	public void setLayers(Set<Layer> layers) {
		this.layers = layers;
		cache.setLayers(layers);
	}

	public void addConstraintException(String className, ConstraintException exception) {
		exceptions.put(className, exception);
	}

	public List<Rule> getRules() {
		return rules;
	}

	@Override
	public String toString() {
		return layers.toString();
	}

	@Override
	public Constraints getConstraints(final String className) {
		try {
			LOG.info("Classname '{}'", className);
			final Unit classUnit = Unit.createClassUnit(className);//Unit.createUnitFromRawFile(className);
			Layer layer = getWorkableLayer(classUnit);

			if (layer == null) {
				LOG.error("Unable to find '{}' in structure definition", classUnit.getName());
			} else {
				return new StructureDefinitionConstraints(layer, classUnit);
			}
		} catch (Exception ex) {
			LOG.error("An exception occured", ex);
		}
		return null;
	}

	/**
	 * Returns a ConstraintException for the given class or null if it doesn't exist.
	 *
	 * @param className
	 * @return
	 */
	@Override
	public ConstraintException getConstraintException(String className) {
		return exceptions.get(className);
	}


	private class StructureDefinitionConstraints implements Constraints {

		private final Layer finalLayer;
		private final Unit classUnit;

		public StructureDefinitionConstraints(Layer finalLayer, Unit classUnit) {
			this.finalLayer = finalLayer;
			this.classUnit = classUnit;
		}

		@Override
		public IConstraintResult allowedToAccess(Unit location) {
			Layer referenceLayer = getWorkableLayer(location);

			IConstraintResult result;
			if (referenceLayer == null) {
				result = new UnknownClassResult();
			} else {
				boolean allowedToAccess = finalLayer.allowedToAccess(referenceLayer);
				result = new BackOrSkipCallConstraintResult(allowedToAccess, finalLayer.getNumber(), referenceLayer.getNumber());
			}

			LOG.info("Is '{}' in layer '{}' allowed to access '{}' in layer '{}': {}", new Object[]{classUnit.getName(), finalLayer, location.getName(), referenceLayer, result.isAllowed()});
			return result;
		}
	}

	public Layer getWorkableLayer(Unit location) {
		return cache.getLayerForUnit(location);
	}
}
